package com.katesoft.scale4j.rttp.spring.postprocessor;

import static com.katesoft.scale4j.persistent.model.unified.BO.PROP_UID;
import static com.katesoft.scale4j.rttp.internal.IRttpHazelcastRefs.ID_GENERATOR_MAP;
import static java.lang.Math.max;
import static org.apache.commons.lang.StringUtils.rightPad;
import static org.hibernate.EntityMode.POJO;
import static org.hibernate.mapping.Table.qualify;
import static org.springframework.jdbc.support.JdbcUtils.closeResultSet;
import static org.springframework.jdbc.support.JdbcUtils.closeStatement;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang.mutable.MutableLong;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.envers.configuration.AuditConfiguration;
import org.hibernate.jdbc.Work;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;

import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import com.katesoft.scale4j.common.services.IBeanNameReferences;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.model.RevisionDomainEntity;
import com.katesoft.scale4j.persistent.model.unified.IBO;
import com.katesoft.scale4j.persistent.spring.EntityManagedOrNativeHibernateFactoryBean;
import com.katesoft.scale4j.persistent.spring.ILocalHibernateSessionBuilder;
import com.katesoft.scale4j.persistent.spring.LocalAnnotationEntityManagerFactory;
import com.katesoft.scale4j.persistent.spring.LocalAnnotationSessionFactory;
import com.katesoft.scale4j.rttp.internal.RttpHazelcastBridgeAwareImpl;

/**
 * This class is created for loading max identifier value from database for each domain entity
 * class.
 * <p/>
 * After this jvmcluster will load this data into memory and id generation will be just
 * in-memory(distributed id generator used for this).
 * 
 * @author kate2007
 */
public class RttpIdInitializerPostProcessor extends RttpHazelcastBridgeAwareImpl implements
         BeanPostProcessor {
   private final Logger logger = LogFactory.getLogger(getClass());

   @Override
   public Object postProcessBeforeInitialization(Object bean,
            @SuppressWarnings("unused") String beanName) throws BeansException {
      return bean;
   }

   @Override
   public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
      if (IBeanNameReferences.SESSION_FACTORY.equalsIgnoreCase(beanName)
               && bean instanceof EntityManagedOrNativeHibernateFactoryBean) {
         ILocalHibernateSessionBuilder hibernateSessionBuilder = ((EntityManagedOrNativeHibernateFactoryBean) bean)
                  .getActualFactoryBuilderBean();
         final Configuration configuration = hibernateSessionBuilder.getConfiguration();
         SessionFactory sessionFactory = (hibernateSessionBuilder instanceof LocalAnnotationEntityManagerFactory) ? ((LocalAnnotationEntityManagerFactory) hibernateSessionBuilder)
                  .getSessionFactory() : ((LocalAnnotationSessionFactory) hibernateSessionBuilder)
                  .getObject();
         HazelcastInstance instance = getBridge().getRunningInstance();
         final IMap<Object, Object> map = instance.getMap(ID_GENERATOR_MAP);

         final HibernateTemplate hibernateTemplate = new HibernateTemplate(sessionFactory);
         hibernateTemplate.execute(new HibernateCallback<Object>() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
               logger.debug("************** Persistent classes mapping **************");
               StringBuilder builder = new StringBuilder();
               Map<String, ClassMetadata> classMetadata = session.getSessionFactory()
                        .getAllClassMetadata();
               for (Entry<String, ClassMetadata> entry : classMetadata.entrySet()) {
                  AbstractEntityPersister meta = (AbstractEntityPersister) entry.getValue();
                  //
                  String entityName = entry.getKey();
                  PersistentClass persistentClass = configuration.getClassMapping(entityName);
                  String tableName = persistentClass.getTable().getName();
                  Class<?> mappedClass = meta.getMappedClass(POJO);
                  boolean isInternal = (IBO.class.isAssignableFrom(mappedClass) || RevisionDomainEntity.class
                           .isAssignableFrom(mappedClass));
                  if (isInternal && !map.containsKey(mappedClass.getName())) {
                     long l = queryMaxIdentifier(persistentClass, session, configuration);
                     logger.debug(String.format("[%s | %s | %s] -> %s", rightPad(tableName, 25),
                              rightPad(entityName, 75), rightPad(mappedClass.getName(), 75), l));
                     map.putIfAbsent(mappedClass.getName(), l);
                  }
               }
               logger.debug(builder.toString());
               logger.debug("********************************************************");
               return null;
            }
         });
      }
      return bean;
   }

   /**
    * looking for max identifier for the given entity class in both table/history table.
    * 
    * @param persistentClass
    *           actual class
    * @param session
    *           active session
    * @param configuration
    *           session factory configuration
    * @return max identifier for the given entity
    * @throws SQLException
    *            re-throw jdbc exceptions
    */
   protected long queryMaxIdentifier(final PersistentClass persistentClass, final Session session,
            Configuration configuration) throws SQLException {
      final MutableLong l1 = new MutableLong(0);
      final MutableLong l2 = new MutableLong(0);
      //
      final Column column = (Column) persistentClass.getProperty(PROP_UID).getColumnIterator()
               .next();
      String t1 = qualify(persistentClass.getTable().getCatalog(), persistentClass.getTable()
               .getSchema(), persistentClass.getTable().getName());
      final String sql1 = String.format("select max(tbl.%s) from %s tbl", column.getName(), t1);
      session.doWork(new Work() {
         @Override
         public void execute(Connection connection) throws SQLException {
            PreparedStatement st1 = null;
            ResultSet rs1 = null;
            try {
               st1 = connection.prepareCall(sql1);
               rs1 = st1.executeQuery();
               if (rs1.next()) {
                  l1.setValue(rs1.getLong(1));
               }
            } catch (SQLException e) {
               logger.error(String.format("unable to execute %s", sql1), e);
               throw e;
            } finally {
               closeResultSet(rs1);
               closeStatement(st1);
            }
         }
      });
      AuditConfiguration auditCfg = AuditConfiguration.getFor(configuration);
      if (auditCfg.getEntCfg().isVersioned(persistentClass.getEntityName())) {
         String t2 = qualify(
                  persistentClass.getTable().getCatalog(),
                  persistentClass.getTable().getSchema(),
                  auditCfg.getAuditEntCfg().getAuditTableName(persistentClass.getEntityName(),
                           persistentClass.getTable().getName()));
         final String sql2 = String.format("select max(tbl.%s) from %s tbl", column.getName(), t2);
         session.doWork(new Work() {
            @Override
            public void execute(Connection connection) throws SQLException {
               PreparedStatement st2 = null;
               ResultSet rs2 = null;
               try {
                  st2 = connection.prepareCall(sql2);
                  rs2 = st2.executeQuery();
                  if (rs2.next()) {
                     l2.setValue(rs2.getLong(1));
                  }
               } catch (SQLException e) {
                  logger.error(String.format("unable to execute %s", sql2), e);
                  throw e;
               } finally {
                  closeResultSet(rs2);
                  closeStatement(st2);
               }
            }
         });
      }
      return max(l1.longValue(), l2.longValue());
   }
}